package me.lwn.auth.security.web.authentication;

import me.lwn.auth.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
import me.lwn.auth.security.utils.Constants;
import me.lwn.auth.security.utils.LocalMessageSource;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

import static me.lwn.auth.security.utils.Constants.SPRING_SECURITY_FORM_CAPTCHA_KEY;

/**
 * 用户登录校验Filter，添加对图形验证码的支持及短信登录支持。
 * 图形验证码部分的说明：登陆页面需要根据session中的require_captcha字段判断是否需要图形验证码，
 * 短信验证码登录时不需要图形验证码校验
 * <p>
 * 特殊说明的是在web登录页的表单中新增了type字段，判断是用户名登录还是短信验证码登录做了如下修改：
 * <p>
 *
 * @author lizhichao
 * @see me.lwn.auth.security.utils.Constants#SPRING_SECURITY_FORM_CAPTCHA_KEY
 * 1、用户名及密码登录时，type=password, 用户名：username, 密码：password, 图形验证码：SPRING_SECURITY_FORM_CAPTCHA_KEY
 * @see me.lwn.auth.security.utils.Constants#SPRING_SECURITY_FORM_SMS_CODE_KEY
 * 2、短信验证码登录时，type=sms, 手机号：mobile, 验证码：SPRING_SECURITY_FORM_SMS_CODE_KEY
 * @since 0.0.1
 */
public class DelegatingLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
            "POST");
    protected MessageSourceAccessor local = LocalMessageSource.getAccessor();
    private String captchaParameter = SPRING_SECURITY_FORM_CAPTCHA_KEY;
    private AuthenticationConverter authenticationConverter;
    private LoginLockedSupport loginLockedSupport;

    public DelegatingLoginAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public DelegatingLoginAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
        authenticationConverter = new DelegatingAuthenticationConverter(Collections.singletonList(
                new UsernamePasswordAuthenticationConverter()
        ));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        //校验图形验证码
        checkCaptcha(request);

        //验证账号是否锁定
        loginLockedSupport.check(request);

        //验证用户名及其凭据
        Authentication authentication = this.authenticationConverter.convert(request);
        return this.getAuthenticationManager().authenticate(authentication);
    }

    @Nullable
    protected String obtainCaptcha(HttpServletRequest request) {
        String code = request.getParameter(this.captchaParameter);
        return (code != null) ? code : "";
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public final String getCaptchaParameter() {
        return this.captchaParameter;
    }

    public void setCaptchaParameter(String captchaParameter) {
        Assert.hasText(captchaParameter, "Captcha parameter must not be empty or null");
        this.captchaParameter = captchaParameter;
    }

    private void checkCaptcha(HttpServletRequest request) {

        String code = obtainCaptcha(request);
        Boolean captcha = (Boolean) request.getSession(false).getAttribute(Constants.ClientSetting.REQUIRE_CAPTCHA.getKey());
        if (captcha != null && captcha) {
            String cacheCode = (String) request.getSession(false).getAttribute(getCaptchaParameter());
            request.getSession(false).removeAttribute(getCaptchaParameter());
            if (!StringUtils.hasText(cacheCode)) {
                throw new CaptchaExpiredException(local.getMessage("DelegatingLoginAuthenticationFilter.captchaExpired"));
            }
            if (!StringUtils.hasText(code) || !cacheCode.equalsIgnoreCase(code)) {
                throw new BadCaptchaException(local.getMessage("DelegatingLoginAuthenticationFilter.badCaptcha"));
            }
        }
    }

    public AuthenticationConverter getAuthenticationConverter() {
        return authenticationConverter;
    }

    public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
        this.authenticationConverter = authenticationConverter;
    }

    public void setLoginLocked(LoginLockedSupport loginLockedSupport) {
        this.loginLockedSupport = loginLockedSupport;
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        loginLockedSupport.remove(request, authResult.getName(), "WEB登录成功");
        super.successfulAuthentication(request, response, chain, authResult);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        if (!(failed.getCause() instanceof LockedException)) {
            loginLockedSupport.count(request, response,
                    StringUtils.hasText(request.getParameter("username")) ? request.getParameter("username") :
                            request.getParameter("mobile"), failed.getMessage());
        }
        super.unsuccessfulAuthentication(request, response, failed);
    }
}
